/*
 * Reksio - Memory Map Editor
 * Copyright (C) 2023 CERN
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * In applying this licence, CERN does not waive the privileges and immunities
 * granted to it by virtue of its status as an Intergovernmental Organization or
 * submit itself to any jurisdiction.
 */

#include "mainwindow.h"
#include "ui_mainwindow.h"

#include "memory_node_yaml.h"
#include "validatornode.h"
#include "pythonaction.h"

#include "searchwindow.h"
#include "settingsdialog.h"
#include "customnodesview.h"
#include "customattributesview.h"
#include "attributecontainervalidator.h"
#include "treenodeoverviewmodel.h"
#include "utils.h"
#include "commands.h"

#include <pybind11/embed.h>

#include <QDebug>
#include <QFileDialog>
#include <QCloseEvent>
#include <QLabel>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QJsonArray>
#include <QStringList>
#include <QStandardPaths>
#include <QDesktopServices>
#include <QMimeData>
#include <QDirIterator>
#include <QSortFilterProxyModel>
#include <QGroupBox>
#include <QDockWidget>
#include <QVBoxLayout>
#include <QTableView>
#include <QHeaderView>
#include <QScrollBar>
#include <QToolBar>
#include <QDateTime>
#include <QInputDialog>
#include <QAbstractButton>
#include <qsystemdetection.h>

QTextEdit * MainWindow::console = nullptr;
MainWindow* MainWindow::main_window = nullptr;

MainWindow::MainWindow(bool setup_console, QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    // init static variable
    main_window = this;

    // settings
    QSettings settings;
    settings.setFallbacksEnabled(false);
    settings.setDefaultFormat(QSettings::Format::IniFormat);

    // init ui
    ui->setupUi(this);

    // setup console and cout forwarding
    if(setup_console)
        setupConsole();

    // docks
    QLabel* label = new QLabel("Central widget");
    label->hide();
    setDockNestingEnabled(true);
    setCentralWidget(label); // takes ownership

    window_title = "[*]" + QCoreApplication::applicationName() + " " + QCoreApplication::applicationVersion();
    window_title += " build " + QString::fromLocal8Bit(TODAY);
    setWindowTitle(window_title);

    // recent files
    ui->menuOpen_recent->clear();
    for(int i=0; i<MaxRecentFiles; ++i)
    {
        recentFileActs[i] = new QAction(this);
        recentFileActs[i]->setVisible(false);
        connect(recentFileActs[i], SIGNAL(triggered()), this, SLOT(openRecentFile()));
        ui->menuOpen_recent->addAction(recentFileActs[i]);
    }
    updateRecentFileActions();

    // default locks around external files edit
    prompt_submap = false;
    prompt_mainmap = false;

    // python
    setupPython();

    loadSchema();

    setupGlobalSettings(qApp->applicationDirPath() + "/settings/settings.json");

    // setup docks
    setupDocks();

    // quit
    ui->actionQuit->setShortcut(QKeySequence::Quit);

    // search window
    search_window = new SearchWindow(this);

    // autosave
    autosave_timer = new QTimer(this);

    // settings dialog
    settings_dialog = new SettingsDialog(this);

    // autosave settings
    connect(autosave_timer, &QTimer::timeout, this, &MainWindow::autosave);
    autosave_path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/autosave";
    QDir out_dir = autosave_path;
    if(!out_dir.exists())
        out_dir.mkpath(autosave_path);

    autosave_files_to_keep = settings.value("autosave/rotation", 10).toInt();
    autosave_timer->setInterval(1000 * 60 * settings.value("autosave/interval", 5).toInt()); // in ms
    if(settings.value("autosave/enabled", true).toBool())
        autosave_timer->start();

    // restore state & geometry
    restoreStateAndGeometry();

    // session
    QGuiApplication::setFallbackSessionManagementEnabled(false);
    connect(qApp, &QGuiApplication::commitDataRequest, this, &MainWindow::commitData);

    fileWatcher = new FileWatcher(this);
    connect(fileWatcher, &FileWatcher::attributesFileChanged, this, &MainWindow::callPythonOnAttributeFileChangeScript);
    connect(fileWatcher, &FileWatcher::rootChanged, this, &MainWindow::callPythonOnRootFileChanged);
}

void MainWindow::closeEvent(QCloseEvent *event)
{
    autosave();
    QMessageBox::StandardButton decision = saveDiscardCancel();
    if(decision == QMessageBox::Cancel)
    {
        event->ignore();
        return;
    }
    // save settings
    saveStateAndGeometry();
    QMainWindow::closeEvent(event);
}

YAML::Node MainWindow::getSchema()
{
    return schema;
}

ValidatorNode* MainWindow::getValidator()
{
    return validator.get();
}


MainWindow::~MainWindow()
{
    streamer.reset();
    delete ui;
    python_modules.reset(); // has to be deleted BEFORE finalization of the interpreter
    delete interpreter_widget;
    pybind11::finalize_interpreter();
}

void MainWindow::addToRecentFiles(const QString &fileName)
{
    QSettings settings;
    QStringList files = settings.value("recentFileList").toStringList();
    files.removeAll(fileName);
    files.prepend(fileName);
    while(files.size() > MaxRecentFiles)
        files.removeLast();

    settings.setValue("recentFileList", files);
    updateRecentFileActions();
}

void MainWindow::updateRecentFileActions()
{
    QSettings settings;
    QStringList files = settings.value("recentFileList").toStringList();
    files.erase(std::remove_if(files.begin(), files.end(), [](const auto& filename){ return !QFile::exists(filename);}), files.end());
    int numRecentFiles = qMin(files.size(), int(MaxRecentFiles));
    for(int i=0; i<numRecentFiles; ++i)
    {
        const QString text = tr("&%1").arg(getFriendlyFilename(files[i]));
        recentFileActs[i]->setText(text);
        recentFileActs[i]->setData(files[i]);
        recentFileActs[i]->setToolTip(files[i]);
        recentFileActs[i]->setStatusTip(files[i]);
        recentFileActs[i]->setVisible(true);
    }
    for(int j=numRecentFiles; j<MaxRecentFiles; ++j)
        recentFileActs[j]->setVisible(false);
    ui->menuOpen_recent->setDisabled(ui->menuOpen_recent->isEmpty());
}

void MainWindow::setupUndoRedo()
{
    undoStack = new QUndoStack(this);

    undoAction = undoStack->createUndoAction(this, tr("&Undo"));
    redoAction = undoStack->createRedoAction(this, tr("&Redo"));
    undoAction->setShortcut(QKeySequence::Undo);
    undoAction->setIcon(QIcon(":/icons/icons/resources/16x16/undo.png"));
    redoAction->setShortcut(QKeySequence::Redo);
    redoAction->setIcon(QIcon(":/icons/icons/resources/16x16/redo.png"));

    ui->menuEdit->addAction(undoAction);
    ui->menuEdit->addAction(redoAction);

    undoView = new QUndoView(this);
    undoView->setStack(undoStack);
    undoView->setWindowFlag(Qt::Window);
    undoView->setWindowTitle(tr("Undo/redo command list"));
    undoView->setAttribute(Qt::WA_QuitOnClose, false);
    undoView->setEmptyLabel(tr("No changes"));
    undoView->setCleanIcon(QIcon(":/icons/icons/resources/16x16/save.png"));
    undoView->setWindowIcon(windowIcon());
    connect(undoStack, &QUndoStack::cleanChanged, this, [this](bool clean){setWindowModified(!clean);});
}

void MainWindow::on_actionOpenUndoRedoView_triggered()
{
    undoView->show();
    undoView->activateWindow();
    undoView->raise();
    undoView->setWindowState(Qt::WindowActive);
}

void MainWindow::on_actionOpenFile_triggered()
{
    const QString& filename = getOpenFileName();
    openFile(filename, false);
}

void MainWindow::reload()
{
    openFile(current_file, true);
}

bool MainWindow::openFile(QString filename, bool skip_modified_check)
{
    if (filename.isEmpty())
		return false;

    if (isModified() && skip_modified_check == false)
    {
        if (saveDiscardCancel() == QMessageBox::Cancel)
		{
            return false;
		}
    }

    clearState();
    QApplication::setOverrideCursor(Qt::WaitCursor);
    try
    {
        filename = QString::fromStdString(callPythonOnBeforeLoadScript(filename.toStdString()));
        load(filename, schema, validator.get());
    }
    catch (const std::exception& e)
    {
        qWarning("%s", e.what());
        clearState();
        QApplication::restoreOverrideCursor();
        return false;
    }

    setDocksAndActionsEnabled(true);
    addToRecentFiles(filename);

    NodesModel* model = getNodesModel();

    fileWatcher->watch(filename, model);

    const QModelIndex & root_item = model->getIndex(model->getRoot()->getChildren().front().get());

    getNodesView()->selectionModel()->setCurrentIndex(root_item, QItemSelectionModel::Select | QItemSelectionModel::Rows);

    QApplication::restoreOverrideCursor();
    return true;
}

void MainWindow::validate()
{
    NodesModel* model = getNodesModel();
    if(!model)
        return; // not yet initialized
    MemoryNode* root = model->getRoot();
    const bool has_deprecated = root->hasDeprecated();
    if(root->validate() && !has_deprecated)
    {
        qInfo("Memory map is valid!");
        // to see later if not too slow
        for(const auto& child: root->getAllChildren())
        {
            const QModelIndex& index = model->getIndex(child);
            Q_EMIT(model->dataChanged(index, index.siblingAtColumn(1), {Qt::ToolTipRole, Qt::ForegroundRole}));
        }
    }
    else
    {
        qWarning("Memory map is NOT valid!");
        std::string message;
        for(const auto& child: root->getAllChildren())
        {
            // to see later if not too slow
            const QModelIndex& index = model->getIndex(child);
            Q_EMIT(model->dataChanged(index, index.siblingAtColumn(1), {Qt::ToolTipRole, Qt::ForegroundRole}));
            // end
            const std::string& node_name = join(child->nodeLocation(), "/");
            std::string error_msg = node_name + " (" + child->getType() + "): ";
            if(!child->pythonValidationResult())
            {
                message += error_msg + child->getValidator()->getPythonValidator()->getMessage() + "\n";
            }
            if(child->hasDeprecated())
            {
                // 1st case, the node itself is deprecated
                if(child->isDeprecated())
                {
                    message += error_msg + "is deprecated!\n";
                    auto deprecated_msg = child->getValidator()->getDeprecatedMessage();
                    if(!deprecated_msg.empty())
                        message += deprecated_msg + "\n";
                }
                // 2nd case, one of the attributes is deprecated
                for(const auto& attr: child->getAttributeContainer()->getAllAttributes())
                {
                    if(attr->isDeprecated())
                    {
                        const std::string& attr_name = join(attr->getFullName(), "/");
                        message += error_msg + "attribute " + attr_name + " is deprecated!\n";
                        auto deprecated_msg = attr->getValidator()->getDeprecatedMessage();
                        if(!deprecated_msg.empty())
                            message += deprecated_msg + "\n";
                    }
                }
                // 3rd case, one of the attribute containers is deprecated
                for(const auto& attr_container: child->getAttributeContainer()->getAllAttributeContainers())
                {
                    if(attr_container->isDeprecated())
                    {
                        const std::string& attr_name = join(attr_container->getFullName(), "/");
                        message += error_msg + "attribute container " + attr_name + " is deprecated!\n";
                        auto deprecated_msg = attr_container->getValidator()->getDeprecatedMessage();
                        if(!deprecated_msg.empty())
                            message += deprecated_msg + "\n";
                    }
                }
            }
            error_msg += " attribute ";
            for(const auto& failed_attr: child->getFailedValidators())
            {
                const std::string& attribute_name = join(failed_attr.first->getFullName(), "/");
                for(const auto& validator: failed_attr.second)
                {
                    message += error_msg + attribute_name + ": " + validator->getMessage() + "\n";
                }
            }
            for(const auto& missing_attr: child->getMissingAttributes())
            {
                const std::string& attr_name = join(missing_attr->getFullName(), "/");
                message += error_msg + attr_name + " is missing!" + "\n";
            }
        }
        qWarning("%s", message.c_str());
    }
}

void MainWindow::openRecentFile()
{
    QAction *action = qobject_cast<QAction *>(sender());
    if (action)
    {
        const QString& filename = action->data().toString();
        openFile(filename, false);
    }
}

QMessageBox::StandardButton MainWindow::getLoadCustomSchemaConfirmation(const QFileInfo& schema_file)
{
	if (!schema_file.exists())
	{
		qDebug() << "Custom schema file " << schema_file.absoluteFilePath() << " not found	- discard.";
		return QMessageBox::No;
	}

	QString title("Reksio - custom schema detected!");

	QString message = "WARNING: using custom schema may cause unexpected Reksio crash!\n";
	message += "Custom schema path: '" + schema_file.absoluteFilePath() + "'.\n";
	message += "Do you want to load a custom schema defined in user setting?";

	return QMessageBox::warning(this, title, message, QMessageBox::Yes | QMessageBox::No);
}

void MainWindow::loadSchema()
{
    schema_ok = false;

    QSettings settings;
    QFileInfo schema_file(qApp->applicationDirPath() + "/schema/schema.yaml");

    const bool useCustomSchema = settings.value("schema/useCustomSchema", false).toBool();

    if (useCustomSchema && settings.contains("schema/customSchemaPath"))
    {
		QFileInfo custom_schema_file(settings.value("schema/customSchemaPath").toString());
		QMessageBox::StandardButton decision = getLoadCustomSchemaConfirmation(custom_schema_file);
        if (decision == QMessageBox::Yes)
        {
            schema_file = custom_schema_file;
			qWarning() << "Custom schema: " << custom_schema_file.absoluteFilePath() << " found.";
        }
        else
        {
            qWarning() << "Custom schema " + custom_schema_file.absoluteFilePath()
						<< " discarded by user or not found. Loading standard one.";
        }
    }

    const QString &fileName = schema_file.absoluteFilePath();
	qDebug() << "Selected schema file: " << fileName << ".";

    std::unique_ptr<ValidatorNode> validator_root_node = std::make_unique<ValidatorNode>("");
    try
    {
        schema = YAML::LoadFile(fileName.toStdString());
        *validator_root_node = schema.as<ValidatorNode>();
        validator.swap(validator_root_node);
        schema_ok = true;
        qInfo() << "Schema " + fileName + " loaded successfully";
    }
    catch (const YAML::BadFile& e)
    {
        std::string msg("Failed to load a schema file " + fileName.toStdString() + ". Error: " + e.what());
        qWarning("%s", msg.c_str());
    }
    catch (const YAML::Exception& e)
    {
        std::string msg("Schema is incorrect! " + fileName.toStdString() + ". Error: " + e.what());
        qWarning("%s", msg.c_str());
    }
    if(!schema_ok && useCustomSchema)
        qInfo() << "You can switch off selected custom schema in the settings menu.";

    ui->actionNewMemoryMap->setEnabled(schema_ok);
    ui->actionOpenFile->setEnabled(schema_ok);
    for(auto& action: recentFileActs)
        action->setEnabled(schema_ok);
}

void MainWindow::on_actionQuit_triggered()
{
    Q_EMIT qApp->closeAllWindows();
}

void MainWindow::on_actionSaveFile_triggered()
{
    for(auto& action: on_save_actions)
    {
        if(callPythonOnSaveAction(action) == QMessageBox::Cancel)
            return;
    }
    if(hasFile())
    {
        save(getOpenedFilename());
    }
    else
    {
        on_actionSaveAs_triggered();
    }
}

void MainWindow::on_actionSaveAs_triggered()
{
    for(auto& action: on_save_actions)
    {
        if(callPythonOnSaveAction(action) == QMessageBox::Cancel)
            return;
    }
    const QString& filename = getSaveFileName();
    if(!filename.isEmpty())
    {
        save(filename);
        addToRecentFiles(filename);
    }
}

void MainWindow::on_actionNewMemoryMap_triggered()
{
    if(isModified())
    {
        QMessageBox::StandardButton decision = saveDiscardCancel();
        if(decision == QMessageBox::Cancel)
            return;
    }
    clearState();
    newMap(validator.get());
    setDocksAndActionsEnabled(true);
}


QMessageBox::StandardButton MainWindow::saveDiscardCancel()
{
    QMessageBox::StandardButton decision = QMessageBox::Cancel;
    if(isModified())
    {
        // save hooks
        for(auto& action: on_save_actions)
        {
            const QMessageBox::StandardButton python_result = callPythonOnSaveAction(action);
            if(python_result == QMessageBox::Cancel)
                return QMessageBox::Cancel;
            if(python_result == QMessageBox::NoToAll)
                break;
            if(python_result == QMessageBox::SaveAll)
                decision = python_result;
        }
        if(decision != QMessageBox::SaveAll)
        {
            if(hasFile())
                decision = getSaveConfirmation(getOpenedFilename());
            else
                decision = getSaveConfirmation();
        }

        if(decision == QMessageBox::Save || decision == QMessageBox::SaveAll)
        {
            if(hasFile())
            {
                save(getOpenedFilename());
            }
            else
            {
                const QString& filename = getSaveFileName();
                if(filename.isEmpty())
                    decision = QMessageBox::Cancel;
                save(filename);
            }
        }
        else if (decision == QMessageBox::NoToAll)
        {
            decision = QMessageBox::Discard;
        }
    }
    else
    {
        decision = QMessageBox::Discard;
    }
    return decision;
}


QString MainWindow::getOpenFileName()
{
    const QString path = QFileDialog::getOpenFileName(this,
                                tr("Open a memory map file"),
                                getCurrentPath(),
                                tr("Memory map file (*.yaml *.cheby);;Legacy memory map file (*.xml);;wb-gen memory map file (*.wb);;All files (*)"));
    if(!path.isEmpty())
    {
        QSettings settings;
        settings.setValue("lastAccessedPath", path);
    }
    return path;
}

QString MainWindow::getSaveFileName()
{
    const QString path = QFileDialog::getSaveFileName(this,
                                tr("Save a memory map file"),
                                getCurrentPath(),
                                tr("Memory map file (*.yaml *.cheby);;All files (*)"));
    if(!path.isEmpty())
    {
        QSettings settings;
        settings.setValue("lastAccessedPath", path);
    }
    return path;
}

QMessageBox::StandardButton MainWindow::getSaveConfirmation(const QString& full_path)
{
    QString message, title;
    if(full_path.isEmpty())
    {
        title = tr("reksio - unsaved changes!");
        message = tr("You have unsaved changes, would you like to save?\n");
    }
    else
    {
        title = tr("reksio - unsaved changes in %1").arg(getFriendlyFilename(full_path));
        message = tr("You have unsaved changes in %1, would you like to save?\n").arg(full_path);
    }
    return QMessageBox::warning(this,
                       title,
                       message,
                       QMessageBox::Save | QMessageBox::SaveAll | QMessageBox::Discard | QMessageBox::Cancel,
                                QMessageBox::Save);

}

QMessageBox::StandardButton MainWindow::getSaveSubmapConfirmation(const QString &full_path)
{
    QString message, title;
    if(full_path.isEmpty())
    {
        title = tr("reksio - unsaved changes!");
        message = tr("You have unsaved changes, would you like to save?\n");
    }
    else
    {
        title = tr("reksio - unsaved changes in %1").arg(getFriendlyFilename(full_path));
        message = tr("You have unsaved changes in %1, would you like to save?\n").arg(full_path);
    }
    QMessageBox msgBox(this);
    msgBox.setWindowTitle(title);
    msgBox.setText(title);
    msgBox.setInformativeText(message);
    msgBox.setIcon(QMessageBox::Icon::Warning);
    msgBox.setStandardButtons(QMessageBox::Save | QMessageBox::SaveAll | QMessageBox::Discard | QMessageBox::NoToAll | QMessageBox::Cancel);
    msgBox.setDefaultButton(QMessageBox::Save);
    // discard all submaps
    auto noToAllBtn = msgBox.button(QMessageBox::NoToAll);
    noToAllBtn->setText(tr("Discard all submaps"));
    return static_cast<QMessageBox::StandardButton>(msgBox.exec());
}

MainWindow *MainWindow::getInstance()
{
    return main_window;
}

QString MainWindow::getOpenedFilename() const
{
    return current_file;
}

QString MainWindow::getCurrentPath() const
{
    if(hasFile())
    {
        return QFileInfo(getOpenedFilename()).path();
    }
    else
    {
        QSettings settings;
        return settings.value("lastAccessedPath", QDir::homePath()).toString();
    }
}

bool MainWindow::isModified() const
{
    return !undoStack->isClean();
}

bool MainWindow::hasFile() const
{
    return !current_file.isEmpty();
}

void MainWindow::save(const QString &fileName)
{
    QApplication::setOverrideCursor(Qt::WaitCursor);
    const bool saving_to_same_file = fileName == current_file;
    if(saving_to_same_file)
    {
        // saving to the same file
        // block file watching since it's us who'll modify the file
        fileWatcher->ignoreNextSignalFromFile(fileName);
    }
    try
    {
        MemoryNode::save(getNodesModel()->getRoot()->getChildren()[0].get(), fileName.toStdString());
    }
    catch (const std::exception& e)
    {
        qWarning("%s", e.what());
        QApplication::restoreOverrideCursor();
        return;
    }
    undoStack->setClean();
    current_file = fileName;
    setWindowTitle(window_title + " " + current_file);
    const QString message("Memory map saved in " + getOpenedFilename());
    ui->statusBar->showMessage(message);
    qInfo() << message;
    QApplication::restoreOverrideCursor();
    if(!saving_to_same_file)
    {
        // saving to a different file
        fileWatcher->changeRootFile(fileName);
    }
}

void MainWindow::load(const QString &fileName, const YAML::Node &schema, ValidatorNode* validator)
{
    // yaml
    // call python before load script
    std::pair<std::unique_ptr<MemoryNode>, std::vector<std::string>> res;
    try
    {
        res = MemoryNode::fromFile(fileName.toStdString(), schema, validator);
    }
    catch (const std::exception& e)
    {
        qWarning("Failed to load memory map %s: %s", fileName.toStdString().c_str(), e.what());
        throw;
    }
    for(const auto& msg: res.second)
    {
        qWarning() << QString::fromStdString(msg);
    }
    callPythonOnLoadScript(res.first.get());
    nodesView->populateModel(std::move(res.first), validator);
    current_file = fileName;
    setWindowTitle(window_title + " " + current_file);
    const QString message("Memory map " + getOpenedFilename() + " loaded.");
    ui->statusBar->showMessage(message);
    qInfo() << message;
    validate();
    callPythonOnLoadedScript();
    nodesView->QTreeView::expandToDepth(0);
}

void MainWindow::newMap(ValidatorNode* validator)
{
    const std::string type = (*validator->getChildren().begin())->getName(); // get first children's name - its root type
    std::unique_ptr<MemoryNode> root(std::make_unique<MemoryNode>(type));

    MemoryNode::bind(root.get(), validator);
    nodesView->populateModel(std::move(root), validator);
    undoStack->beginMacro("Added missing attributes to the new " + QString::fromStdString(type) + ".");
    nodesView->addRequiredAttributes(getNodesModel()->index(0, 0));
    undoStack->endMacro();
    validate();
}

void MainWindow::itemChanged(MemoryNode *item)
{
    const QModelIndex& index = getNodesModel()->getIndex(item);
    Q_EMIT getNodesModel()->dataChanged(index, index);
}

void MainWindow::itemChanged(Attribute *item)
{
    const QModelIndex& parent_index = getNodesModel()->getIndex(item->getParentNode());
    AttributesModel* attributes_model = getAttributesModel(parent_index);
    const QModelIndex& index = attributes_model->getIndex(item);
    Q_EMIT attributes_model->dataChanged(index, index);
}

NodesModel *MainWindow::getNodesModel()
{
    return nodesView->getNodesModel();
}

AttributesModel *MainWindow::getAttributesModel(const QModelIndex &index)
{
    return nodesView->getAttributesModel(index);
}

AttributesModel *MainWindow::getCurrentAttributesModel()
{
    return attributesView->getAttributesModel();
}

CustomNodesView *MainWindow::getNodesView()
{
    return nodesView;
}

FileWatcher *MainWindow::getFileWatcher()
{
    return fileWatcher;
}

void MainWindow::commitData(QSessionManager &manager)
{
    if(manager.allowsInteraction())
    {
        QMessageBox::StandardButton decision = saveDiscardCancel();
        if (decision == QMessageBox::Cancel)
        {
            manager.cancel();
        }
    }
    else
    {
        autosave();
    }
}

void MainWindow::clearState()
{
    qDeleteAll(childrenOverview->findChildren<QWidget*>(QString(), Qt::FindDirectChildrenOnly));
    nodesView->clear();
    attributesView->setModel(nullptr);
    childrenList->clear();
    attributeList->clear();
//    console->clear();
    undoStack->clear();
    undoStack->setClean();
    current_file = "";
    setWindowTitle(window_title);
    setDocksAndActionsEnabled(false);
    autosave_undoStack_index = -666;
    fileWatcher->clear();
}

QUndoStack *MainWindow::getUndoStack()
{
    return undoStack;
}

void MainWindow::nodesViewCurrentChanged(const QModelIndex &index)
{
    NodesModel* model = getNodesModel();
    MemoryNode* node = model->getItem(index);
    AttributesModel * attrib_model = model->getAttributesModel(index);

    //new QAbstractItemModelTester(attrib_model, QAbstractItemModelTester::FailureReportingMode::Warning, this);

    // signal handling (rowsInserted/rowsRemoved/dataChanged) done in NodesModel::getAttributesModel

    attributesView->setModel(attrib_model);
    attributesView->expandToDepth(1);
    // children list
    childrenList->clear();

    std::function<void(QTreeWidgetItem*, ValidatorNode*)> fn;
    fn = [&fn, node](QTreeWidgetItem* parent, ValidatorNode* validator){
        if(validator->isSpecialSequence())
        {
            for(const auto& child: validator->getChildren())
            {
                const QString& name = QString::fromStdString(child->getName());
                const QString& relname = QString::fromStdString(join(child->getRelativeName(node->getValidator()), "/"));
                QTreeWidgetItem * item = new QTreeWidgetItem(parent, {name});
                if(child->isDeprecated())
                {
                    item->setForeground(0, QBrush(QColor(Qt::GlobalColor::darkYellow)));
                    QString tooltip_msg = "This item is deprecated!\n";
                    auto deprecated_msg = child->getDeprecatedMessage();
                    if(!deprecated_msg.empty())
                    {
                        tooltip_msg += QString::fromStdString(deprecated_msg);
                    }
                    item->setToolTip(0, tooltip_msg);
                }
                item->setData(0, Qt::UserRole, relname);
                fn(item, child);
            }
        }
    };
    // root level
    for(const auto& child: node->getValidator()->getChildren())
    {
        const QString& name = QString::fromStdString(child->getName());
        QTreeWidgetItem* root = new QTreeWidgetItem(childrenList, {name});
        if(child->isDeprecated())
        {
            root->setForeground(0, QBrush(QColor(Qt::GlobalColor::darkYellow)));
            QString tooltip_msg = "This item is deprecated!\n";
            auto deprecated_msg = child->getDeprecatedMessage();
            if(!deprecated_msg.empty())
            {
                tooltip_msg += QString::fromStdString(deprecated_msg);
            }
            root->setToolTip(0, tooltip_msg);
        }
        root->setData(0, Qt::UserRole, name);
        if(child->isSpecialSequence())
        {
            fn(root, child);
        }
    }

    // children overview
    qDeleteAll(childrenOverview->findChildren<QWidget*>(QString(), Qt::FindDirectChildrenOnly));
    for(const auto& child : node->getValidator()->getChildrenWithSpecialSequences())
    {
        const bool child_from_special_sequence = child->getParent()->isSpecialSequence();
        QString child_name_qstr;
        const std::vector<std::string> relative_name = child->getRelativeName(node->getValidator());

        if(child_from_special_sequence)
        {
            child_name_qstr = QString::fromStdString(join(relative_name, "/"));
        }
        else
        {
            child_name_qstr = QString::fromStdString(child->getName());
        }

        if(node->hasChild(relative_name))
        {
            QGroupBox* box = new QGroupBox(childrenOverview);
            box->setLayout(new QHBoxLayout); // takes ownership
            box->setTitle(child_name_qstr);
            box->setFlat(true);
            box->layout()->setMargin(0);

            QTableView *overview = new QTableView(box);
            overview->setAlternatingRowColors(true);
            overview->horizontalHeader()->setSectionsMovable(true);
            //attributesView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeMode::ResizeToContents);
            overview->setSortingEnabled(true);
            QSortFilterProxyModel *proxyModel = new QSortFilterProxyModel(overview);
            TreeNodeOverviewModel * overviewModel;
            if(child_from_special_sequence)
            {
                // get child's parent
                MemoryNode* child_parent = node->getChild(relative_name)->getParent();
                overviewModel = new TreeNodeOverviewModel(child_parent, child->getName(), overview);
            }
            else
            {
                overviewModel = new TreeNodeOverviewModel(node, child->getName(), overview);
            }
            proxyModel->setSourceModel(overviewModel);
            proxyModel->setSortRole(TreeNodeOverviewModel::SortRole);
            overview->setModel(proxyModel);
            overview->verticalHeader()->setDefaultSectionSize(overview->verticalHeader()->minimumSectionSize());
            overview->resizeColumnsToContents();
            verticalResizeTableViewToContents(overview);
            box->layout()->addWidget(overview);
            childrenOverview->layout()->addWidget(box);
            connect(overview, &QTableView::clicked, this, &MainWindow::onChildrenOverviewNode_clicked);
        }
    }
    // attributes list
    refillAttributesList();
}

void MainWindow::nodesViewSelectionChanged(const QItemSelection &selected, const QItemSelection &/*deselected*/)
{
    const QModelIndexList& indexes = selected.indexes();
    if(indexes.size() == 2)
    {
        nodesViewCurrentChanged(indexes[0]);
    }
}

void MainWindow::nodesViewModelChanged()
{
    connect(nodesView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &MainWindow::nodesViewSelectionChanged);
    connect(getNodesModel(), &NodesModel::rowsInserted, this, &MainWindow::onNodesInserted, Qt::QueuedConnection);
    connect(getNodesModel(), &NodesModel::rowsRemoved, this, &MainWindow::onNodesRemoved, Qt::QueuedConnection);
    connect(getNodesModel(), &NodesModel::rowsMoved, this, &MainWindow::onNodesMoved, Qt::QueuedConnection);
}

void MainWindow::onChildrenOverviewNode_clicked(const QModelIndex &index)
{
    const QSortFilterProxyModel* proxy_model = static_cast<const QSortFilterProxyModel*>(index.model());
    const QModelIndex& real_index = proxy_model->mapToSource(index);
    MemoryNode* node = static_cast<MemoryNode*>(real_index.internalPointer());
    const QModelIndex& nodesModelIndex = getNodesModel()->getIndex(node);
    if(nodesModelIndex.isValid())
    {
        nodesView->selectionModel()->select(nodesModelIndex, QItemSelectionModel::ClearAndSelect);
        // crashes if QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows
        nodesView->scrollTo(nodesModelIndex);
    }
}

void MainWindow::onAttributeChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &/*roles*/)
{
    QItemSelectionRange selection_range(topLeft, bottomRight);
    AttributesModel* model = getAttributesModel(topLeft);
    for(auto& index: selection_range.indexes())
    {
        if(index.column() == 0) // only first column
        {
            if(model->isAttribute(index))
            {
               callPythonOnChangeScript(index);
               fileWatcher->attributeChanged(index);
            }
        }
    }
}

void MainWindow::onNodesInserted(const QModelIndex &parent, int first, int last)
{
    callPythonOnNodesInsertedScript(parent, first, last);
}

void MainWindow::onNodesRemoved(const QModelIndex &parent, int first, int last)
{
    callPythonOnNodesRemovedScript(parent, first, last);
    refillAttributesList();
}

void MainWindow::onNodesMoved(const QModelIndex &parent, int start, int end, const QModelIndex &destination, int row)
{
    callPythonOnNodesMovedScript(parent, start, end, destination, row);
}

void MainWindow::onAttributesInserted(AttributesModel* model, const QModelIndex &parent, int first, int last)
{
    callPythonOnAttributesInsertedScript(parent, first, last);
    refillAttributesList();
    fileWatcher->attributeInserted(model, parent, first, last);
}

void MainWindow::onAttributesRemoved(AttributesModel* model, const QModelIndex &parent, int first, int last)
{
	static_cast<void>(model); // suppress -Wunused-parameter
    callPythonOnAttributesRemovedScript(parent, first, last);
    refillAttributesList();
}

QString MainWindow::getFriendlyFilename(const QString &filename) const
{
    return QFileInfo(filename).fileName();
}

void MainWindow::on_actionClean_console_triggered()
{
    console->clear();
}

void MainWindow::on_callPythonScript()
{
    callPythonScript(qobject_cast<QAction*>(sender()));
}

void MainWindow::callPythonScript(const QAction * action)
{
    if(action)
    {
        const QVariant& data = action->data();
        if(data.isValid() && data.canConvert<PythonAction>())
        {
            const PythonAction& pyAction = data.value<PythonAction>();
            // get argument values
            python_action(pyAction.module.toStdString(), pyAction.function.toStdString(), this);
        }
    }
}

QMessageBox::StandardButton MainWindow::callPythonOnSaveAction(QAction *action)
{
    QMessageBox::StandardButton result = QMessageBox::StandardButton::Discard;
    try
    {
        const QVariant& data = action->data();
        if(data.isValid() && data.canConvert<PythonAction>())
        {
            const PythonAction& pyAction = data.value<PythonAction>();
            return python_action(pyAction.module.toStdString(), pyAction.function.toStdString(), this).cast<QMessageBox::StandardButton>();
        }
    }
    catch (...)
    {
    }
    return result;
}

void MainWindow::callPythonOnLoadScript(MemoryNode *node)
{
    for(const auto& onLoad_action: on_load_actions)
    {
        const QVariant& data = onLoad_action->data();
        if(data.isValid() && data.canConvert<PythonAction>())
        {
            const PythonAction& pyAction = data.value<PythonAction>();
            python_action(pyAction.module.toStdString(), pyAction.function.toStdString(), this, node);
        }
    }
}

void MainWindow::callPythonOnLoadedScript()
{
    for(const auto& onLoad_action: on_loaded_actions)
    {
        const QVariant& data = onLoad_action->data();
        if(data.isValid() && data.canConvert<PythonAction>())
        {
            const PythonAction& pyAction = data.value<PythonAction>();
            python_action(pyAction.module.toStdString(), pyAction.function.toStdString(), this);
        }
    }
}

std::string MainWindow::callPythonOnBeforeLoadScript(const std::string &filename)
{
    std::string new_filename = filename;
    if(on_before_load_action)
    {
        const QVariant& data = on_before_load_action->data();
        if(data.isValid() && data.canConvert<PythonAction>())
        {
            const PythonAction& pyAction = data.value<PythonAction>();
            try
            {
                std::string result = python_action(pyAction.module.toStdString(), pyAction.function.toStdString(), filename).cast<std::string>();
                if(result.empty())
                    throw std::runtime_error("Python before load action halted opening the file.");
                return result;
            }
            catch (const pybind11::cast_error&)
            {
                // this happens when there's an exception in python's "before action", we get bool(false) and we try to cast it to a string
                return new_filename;
            }
        }
    }
    return new_filename;
}

void MainWindow::callPythonOnChangeScript(QModelIndex &index)
{
    for(const auto& onChange_action: on_change_actions)
    {
        const QVariant& data = onChange_action->data();
        if(data.isValid() && data.canConvert<PythonAction>())
        {
            const PythonAction& pyAction = data.value<PythonAction>();
            python_action(pyAction.module.toStdString(), pyAction.function.toStdString(), this, index);
        }
    }
}

void MainWindow::callPythonOnAttributeFileChangeScript(const QModelIndex &index)
{
    for(const auto& onChange_action: on_attributes_file_changed)
    {
        const QVariant& data = onChange_action->data();
        if(data.isValid() && data.canConvert<PythonAction>())
        {
            const PythonAction& pyAction = data.value<PythonAction>();
            python_action(pyAction.module.toStdString(), pyAction.function.toStdString(), this, index);
        }
    }
}

void MainWindow::callPythonOnRootFileChanged()
{
    for(const auto& onChange_action: on_root_file_changed)
    {
        const QVariant& data = onChange_action->data();
        if(data.isValid() && data.canConvert<PythonAction>())
        {
            const PythonAction& pyAction = data.value<PythonAction>();
            python_action(pyAction.module.toStdString(), pyAction.function.toStdString(), this);
        }
    }
}

void MainWindow::callPythonOnNodesInsertedScript(const QModelIndex &parent, int first, int last)
{
    for(const auto& action: on_nodes_inserted_actions)
    {
        const QVariant& data = action->data();
        if(data.isValid() && data.canConvert<PythonAction>())
        {
            const PythonAction& pyAction = data.value<PythonAction>();
            python_action(pyAction.module.toStdString(), pyAction.function.toStdString(), this, parent, first, last);
        }
    }
}

void MainWindow::callPythonOnNodesRemovedScript(const QModelIndex &parent, int first, int last)
{
    for(const auto& action: on_nodes_removed_actions)
    {
        const QVariant& data = action->data();
        if(data.isValid() && data.canConvert<PythonAction>())
        {
            const PythonAction& pyAction = data.value<PythonAction>();
            python_action(pyAction.module.toStdString(), pyAction.function.toStdString(), this, parent, first, last);
        }
    }
}

void MainWindow::callPythonOnNodesMovedScript(const QModelIndex &parent, int start, int end, const QModelIndex &destination, int row)
{
    for(const auto& action: on_nodes_moved_actions)
    {
        const QVariant& data = action->data();
        if(data.isValid() && data.canConvert<PythonAction>())
        {
            const PythonAction& pyAction = data.value<PythonAction>();
            python_action(pyAction.module.toStdString(), pyAction.function.toStdString(), this, parent, start, end, destination, row);
        }
    }
}

void MainWindow::callPythonOnAttributesInsertedScript(const QModelIndex &parent, int first, int last)
{
    for(const auto& action: on_attributes_inserted_actions)
    {
        const QVariant& data = action->data();
        if(data.isValid() && data.canConvert<PythonAction>())
        {
            const PythonAction& pyAction = data.value<PythonAction>();
            python_action(pyAction.module.toStdString(), pyAction.function.toStdString(), this, parent, first, last);
        }
    }
}

void MainWindow::callPythonOnAttributesRemovedScript(const QModelIndex &parent, int first, int last)
{
    for(const auto& action: on_attributes_removed_actions)
    {
        const QVariant& data = action->data();
        if(data.isValid() && data.canConvert<PythonAction>())
        {
            const PythonAction& pyAction = data.value<PythonAction>();
            python_action(pyAction.module.toStdString(), pyAction.function.toStdString(), this, parent, first, last);
        }
    }
}



void MainWindow::setupGlobalSettings(const QString &filename)
{
    QFile settings_file(filename);

    if(!settings_file.open(QIODevice::ReadOnly))
    {
        qWarning("Could not open settings file");
        return;
    }
    const QByteArray& data = settings_file.readAll();
    //qDebug() << data;
    const QJsonDocument doc(QJsonDocument::fromJson(data));
    settings_file.close();
    // python paths
    const QJsonObject python = doc.object().value("python").toObject();

#   if defined(Q_OS_WIN)
    const QJsonObject os = python.value("windows").toObject();
#   else
    const QJsonObject os = python.value("linux").toObject();
#   endif
    for(const auto& path: os.value("paths").toArray())
    {
        const QString& python_site_packages = QDir::toNativeSeparators(path.toString());
        if(QDir(python_site_packages).exists())
            addPythonPath(python_site_packages);
        else
            qWarning() << "Python site-packages path ( " + python_site_packages + " ) was not found";
    }
    // actions
    const QJsonArray actions = doc.object().value("actions").toArray();
    for(const auto& json_action: actions)
    {
        const QJsonObject action_data = json_action.toObject().value("action").toObject();
        // mandatory
        const QString& name = action_data.value("name").toString();
        const QString& module = action_data.value("module").toString();
        const QString& function = action_data.value("function").toString();
        const QString& hook = action_data.value("hook").toString();

        // create PythonAction
        PythonAction py_action(name, module, function);

        const QString& tooltip = action_data.value("tooltip").toString();
        const QString& icon = action_data.value("icon").toString();

        const QString& place = action_data.value("place").toString();

        const QString& shortcut = action_data.value("shortcut").toString();

        QAction* action = new QAction(name, this);

        if(!icon.isNull())
            action->setIcon(QIcon(icon));

        if(!tooltip.isNull())
            action->setToolTip(tooltip);

        if(!shortcut.isNull())
            action->setShortcut(shortcut);

        action->setData(QVariant::fromValue(py_action));
        connect(action, &QAction::triggered, this, &MainWindow::on_callPythonScript);


        if(place == "menuActions")
        {
            ui->menuActions->addAction(action);
        }
        else if(place == "buttonsLayout")
        {
            python_buttons.append(action);
        }

        if(hook == "onSave")
        {
            on_save_actions.append(action);
        }
        else if (hook == "onClick")
        {
            // do nothing
        }
        else if(hook == "onAttributesFileChanged")
        {
            on_attributes_file_changed.append(action);
        }
        else if(hook == "onRootFileChanged")
        {
            on_root_file_changed.append(action);
        }
        else if(hook == "onExit")
        {
            on_exit_actions.append(action);
        }
        else if(hook == "onChange")
        {
            on_change_actions.append(action);
        }
        else if(hook == "onLoad")
        {
            on_load_actions.append(action);
        }
        else if(hook == "onLoaded")
        {
            on_loaded_actions.append(action);
        }
        else if (hook == "onBeforeLoad")
        {
            on_before_load_action = action;
        }
        else if(hook == "onNodesInserted")
        {
            on_nodes_inserted_actions.append(action);
        }
        else if (hook == "onNodesRemoved")
        {
            on_nodes_removed_actions.append(action);
        }
        else if (hook == "onNodesMoved")
        {
            on_nodes_moved_actions.append(action);
        }
        else if (hook == "onAttributesInserted")
        {
            on_attributes_inserted_actions.append(action);
        }
        else if (hook == "onAttributesRemoved")
        {
            on_attributes_removed_actions.append(action);
        }
        else
        {
            qWarning() << "Unknown hook: " + hook;
        }

        python_modules->import(module.toStdString());
    }
}

void MainWindow::on_actionReload_Python_modules_triggered()
{
    try
    {
        python_modules->reload();
    }
    catch (const py::error_already_set& e)
    {
        qWarning() << "Reload action failed: " + QString(e.what());
    }
}

void MainWindow::on_actionFind_triggered()
{
    search_window->show();
    search_window->activateWindow();
    search_window->raise();
    search_window->setWindowState(Qt::WindowActive);
}

void MainWindow::console_contextMenuEvent(const QPoint &pos)
{
    QMenu* menu = console->createStandardContextMenu();
    menu->addAction("Clear console", this, &MainWindow::on_actionClean_console_triggered);
    menu->exec(console->viewport()->mapToGlobal(pos));
    delete menu;
}

void MainWindow::autosave()
{
    const int new_autosave_undostack_index = undoStack->index();
    QDir out_dir = autosave_path;
    const QString& datetime = QDateTime::currentDateTime().toString("yyyy_MM_dd_hh_mm_ss");

    const QFileInfoList& autosave_files = out_dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot, QDir::Time); // starting from newest
    for(int index = autosave_files_to_keep-1; index < autosave_files.size(); ++index)
    {
        const QFileInfo& info = autosave_files.at(index);
        QFile::remove(info.absoluteFilePath());
    }
    if(isModified() && autosave_undoStack_index != new_autosave_undostack_index)
    {
        MemoryNode* root = getNodesModel()->getRoot()->getChildren()[0].get();
        QString filename = "autosave_" + datetime;
        if(hasFile())
        {
            filename += "_" + QFileInfo(getOpenedFilename()).baseName();
        }
        else if (!root->getName().empty())
        {
            filename += "_" + QString::fromStdString(root->getName());
        }
        filename += ".yaml";
        filename = QDir::cleanPath(out_dir.filePath(filename));
        try
        {
            root->save(filename.toStdString());
        }
        catch (const std::exception& e)
        {
            std::string msg = "Failed to autosave " + filename.toStdString() + ": " + e.what();
            qWarning("%s", msg.c_str());
        }
    }
    autosave_undoStack_index = new_autosave_undostack_index;
}

void MainWindow::dragEnterEvent(QDragEnterEvent *event)
{
    const QMimeData* mimeData = event->mimeData();
    if (mimeData->hasUrls())
    {
        event->acceptProposedAction();
    }
}

void MainWindow::dropEvent(QDropEvent *event)
{
    const QMimeData* mimeData = event->mimeData();
    if (mimeData->hasUrls())
    {
        const QList<QUrl>& urlList = mimeData->urls();
        for(const auto& url: urlList)
        {
            QFileInfo path(url.toLocalFile());
            if(path.exists())
            {
                if(path.isFile())
                {
                    if(path.completeSuffix() == "yaml" || path.completeSuffix() == "cheby" || path.completeSuffix() == "xml" || path.completeSuffix() == "wb")
                    {
                        openFile(path.absoluteFilePath(), false);
                        event->acceptProposedAction();
                    }
                }
                else if (path.isDir())
                {
//                    QDirIterator it(path.absolutePath(), QStringList() << "*.yaml", QDir::Files, QDirIterator::Subdirectories);
//                    while(it.hasNext())
//                    {
//                        openFile(it.next(), false);
//                    }
                }
            }
        }
    }
}

void MainWindow::refillAttributesList()
{
    NodesModel* model = getNodesModel();
    MemoryNode* node = model->getItem(nodesView->currentIndex());
    attributeList->clear();
    AttributeContainerValidator* validator = node->getValidator()->getAttributes();
    std::function<void(QTreeWidgetItem *, AttributeContainerValidator*)> fn;
    fn = [&fn, node](QTreeWidgetItem * parent, AttributeContainerValidator* validator){
        for(const auto& attribute: validator->getAttributes())
        {
            // add attributes
            QTreeWidgetItem * item = new QTreeWidgetItem(parent, {QString::fromStdString(attribute->getName())});
            // tooltip text
            QString tooltip_msg;
            const auto& full_name = attribute->getFullName();
            const bool attr_in_node = node->getAttributeContainer()->hasAttribute(full_name);
            // cannot be added or already there - not clickable
            if(!attribute->isAddable() || attr_in_node)
                item->setFlags(item->flags() & ~Qt::ItemIsEnabled);
            // if item cannot be added, make it italic
            if(!attribute->isAddable())
            {
                QFont font;
                font.setItalic(true);
                item->setFont(0, font);
            }
            if(attribute->isDeprecated())
            {
                item->setForeground(0, QBrush(QColor(Qt::GlobalColor::darkYellow)));
                tooltip_msg += "This item is deprecated!\n";
                auto deprecated_msg = attribute->getDeprecatedMessage();
                if(!deprecated_msg.empty())
                {
                    tooltip_msg += QString::fromStdString(deprecated_msg);
                }
            }
            // tooltip
            tooltip_msg += QString::fromStdString(attribute->getToolTip());
            item->setToolTip(0, tooltip_msg);
            item->setData(0, Qt::UserRole, QString::fromStdString(join(full_name, "/")));
        }
        for(const auto& container: validator->getAttributeContainers())
        {
            QTreeWidgetItem * item = new QTreeWidgetItem(parent, {QString::fromStdString(container->getName())});
            fn(item, container.get());
            if(item->childCount() == 0)
            {
                parent->removeChild(item);
            }
        }
    };
    // top level
    for(const auto& attribute: validator->getAttributes())
    {
        // add attributes
        QTreeWidgetItem * item = new QTreeWidgetItem(attributeList, {QString::fromStdString(attribute->getName())});
        // tooltip text
        QString tooltip_msg;
        const auto& full_name = attribute->getFullName();
        const bool attr_in_node = node->getAttributeContainer()->hasAttribute(full_name);
        // cannot be added or already there - not clickable
        if(!attribute->isAddable() || attr_in_node)
            item->setFlags(item->flags() & ~Qt::ItemIsEnabled);
        // if item cannot be added, make it italic
        if(!attribute->isAddable())
        {
            QFont font;
            font.setItalic(true);
            item->setFont(0, font);
        }
        if(attribute->isDeprecated())
        {
            item->setForeground(0, QBrush(QColor(Qt::GlobalColor::darkYellow)));
            tooltip_msg += "This item is deprecated!\n";
            auto deprecated_msg = attribute->getDeprecatedMessage();
            if(!deprecated_msg.empty())
            {
                tooltip_msg += QString::fromStdString(deprecated_msg);
            }
        }
        // tooltip
        tooltip_msg += QString::fromStdString(attribute->getToolTip());
        item->setToolTip(0, tooltip_msg);
        item->setData(0, Qt::UserRole, QString::fromStdString(join(full_name, "/")));
    }
    for(const auto& container: validator->getAttributeContainers())
    {
        QTreeWidgetItem * item = new QTreeWidgetItem(attributeList, {QString::fromStdString(container->getName())});
        QString tooltip_msg;
        if(container->isDeprecated())
        {
            item->setForeground(0, QBrush(QColor(Qt::GlobalColor::darkYellow)));
            tooltip_msg += "This item is deprecated!\n";
            auto deprecated_msg = container->getDeprecatedMessage();
            if(!deprecated_msg.empty())
            {
                tooltip_msg += QString::fromStdString(deprecated_msg);
            }
        }
        item->setToolTip(0, tooltip_msg);
        fn(item, container.get());
        if(item->childCount() == 0)
        {
            attributeList->removeItemWidget(item, 0);
        }
    }
    attributeList->expandAll();
}

void MainWindow::saveStateAndGeometry()
{
    QSettings settings;
    if(settings.value("restore", false).toBool())
    {
        settings.remove("mainWindow");
        settings.remove("restore");
    }
    else
    {
        settings.beginGroup("mainWindow");
        settings.setValue("geometry", QMainWindow::saveGeometry());
        settings.setValue("state", QMainWindow::saveState());
        settings.endGroup();
    }
}

void MainWindow::restoreStateAndGeometry()
{
    QSettings settings;
    settings.beginGroup("mainWindow");
    restoreGeometry(settings.value("geometry").toByteArray());
    restoreState(settings.value("state").toByteArray());
    settings.endGroup();
}

void MainWindow::setupDocks()
{
    setDockOptions(QMainWindow::AnimatedDocks | QMainWindow::AllowNestedDocks | QMainWindow::AllowTabbedDocks | QMainWindow::GroupedDragging);
    setupUndoRedo();
    setupNodesView();
    setupAttributesView(); // requires nodesView present
    setupAttributeList();
    setupChildrenList();
    setupOverview();
    setupConsoleDock();
    setupPythonInterpreterWidgetDock();
    setupToolBar();
    tabifyDockWidget(attributeListDock, childrenListDock);
    setDocksAndActionsEnabled(false);
}

void MainWindow::setupConsoleDock()
{
    consoleDock = new QDockWidget(tr("Console"), this);
    consoleDock->setObjectName(tr("Console"));
    consoleDock->setWidget(console);
    addDockWidget(Qt::LeftDockWidgetArea, consoleDock);
    ui->menuView->addAction(consoleDock->toggleViewAction());
}

void MainWindow::setupConsole()
{
    console = new QTextEdit(this);
    console->setReadOnly(true);
    console->setContextMenuPolicy(Qt::CustomContextMenu);
    connect(console, &QTextEdit::customContextMenuRequested, this, &MainWindow::console_contextMenuEvent);
    // initialize AFTER console is initialized
    streamer = std::make_unique<ConsoleStream>(std::cout);
    streamer->registerMyConsoleMessageHandler();
}

void MainWindow::setupPythonInterpreterWidgetDock()
{
    interpreter_widget = new PythonInterpreterWidget(this);

    pythonInterpreterWidgetDock = new QDockWidget(tr("Python interpreter"), this);
    pythonInterpreterWidgetDock->setObjectName(tr("Python interpreter"));
    pythonInterpreterWidgetDock->setWidget(interpreter_widget);
    addDockWidget(Qt::LeftDockWidgetArea, pythonInterpreterWidgetDock);
    ui->menuView->addAction(pythonInterpreterWidgetDock->toggleViewAction());
}

void MainWindow::setupPython()
{
    pybind11::initialize_interpreter();
    // add python_scripts to python's path
    addPythonPath(qApp->applicationDirPath() + "/python_scripts");

    // custom python path
    QSettings settings;
    const QString& customPaths = settings.value("python/customPath", "").toString();
    if(!customPaths.isEmpty())
    {
        const QStringList& dirs = customPaths.split(";", Qt::SkipEmptyParts);
        for(const auto& dir : dirs)
        {
            QDir py_dir{dir.trimmed()};
            if(py_dir.isAbsolute() && py_dir.exists())
            {
                addPythonPath(py_dir.path());
                qDebug() << "Added custom python path: '" + py_dir.path() + "'.";
            }
            else
            {
                qWarning() << "Custom python path '" + py_dir.path() + "' is not correct. Skipping.";
            }
        }
    }

#if defined(Q_OS_LINUX)
	// Extra python path for Reksio releases for open-source packages for external users.
	// This location is dedicated for linux users (mainly intended for Linux packages).
    addPythonPath(qApp->applicationDirPath() + "/python_scripts/lib/site-packages");
#endif

    python_modules = std::make_unique<PythonModules>();
}

void MainWindow::addPythonPath(const QString &path)
{
    auto pathlib = pybind11::module::import("pathlib");
    auto py_path = pathlib.attr("Path")(path.toStdString());
    // site (adds to sys.path automatically)
    auto site = pybind11::module::import("site");
    site.attr("addsitedir")(pybind11::str(py_path));
}

void MainWindow::setupNodesView()
{
    nodesView = new CustomNodesView(this);
    nodesView->setUndoStack(undoStack);
    //connect(nodesView, &CustomNodesView::clicked, this, &MainWindow::nodesViewCurrentChanged);
    connect(nodesView, &CustomNodesView::modelChanged, this, &MainWindow::nodesViewModelChanged);
    nodesViewDock = new QDockWidget(tr("Nodes tree"), this);
    nodesViewDock->setObjectName(tr("Nodes tree"));
    nodesViewDock->setWidget(nodesView);
    addDockWidget(Qt::LeftDockWidgetArea, nodesViewDock);
    ui->menuView->addAction(nodesViewDock->toggleViewAction());
}

void MainWindow::setupAttributesView()
{
    attributesView = new CustomAttributesView(nodesView, this);
    attributesView->setUndoStack(undoStack);
    attributesViewDock = new QDockWidget(tr("Attributes"), this);
    attributesViewDock->setObjectName(tr("Attributes"));
    attributesViewDock->setWidget(attributesView);
    addDockWidget(Qt::RightDockWidgetArea, attributesViewDock);
    ui->menuView->addAction(attributesViewDock->toggleViewAction());
}

void MainWindow::setupAttributeList()
{
    attributeList = new QTreeWidget(this);
    attributeList->setAlternatingRowColors(true);
    attributeList->setHeaderHidden(true);

    connect(attributeList, &QTreeWidget::itemDoubleClicked, this,
            [this](QTreeWidgetItem* item)
    {
        if(!item->childCount())
            nodesView->addAttribute(item->data(0, Qt::UserRole).toString());
    });

    attributeListDock = new QDockWidget(tr("Available attributes"), this);
    attributeListDock->setObjectName(tr("Available attributes"));
    attributeListDock->setWidget(attributeList);
    addDockWidget(Qt::LeftDockWidgetArea, attributeListDock);
    ui->menuView->addAction(attributeListDock->toggleViewAction());
}

void MainWindow::setupChildrenList()
{
    childrenList = new QTreeWidget(this);
    childrenList->setAlternatingRowColors(true);
    childrenList->setHeaderHidden(true);

    connect(childrenList, &QTreeWidget::itemDoubleClicked, this,
            [this](QTreeWidgetItem* item)
    {
        if(!item->childCount())
            nodesView->addChild(item->data(0, Qt::UserRole).toString());
    });

    childrenListDock = new QDockWidget(tr("Available children"), this);
    childrenListDock->setObjectName(tr("Available children"));
    childrenListDock->setWidget(childrenList);
    addDockWidget(Qt::LeftDockWidgetArea, childrenListDock);
    ui->menuView->addAction(childrenListDock->toggleViewAction());
}

void MainWindow::setupOverview()
{
    childrenOverviewScrollArea = new QScrollArea(this);
    childrenOverviewScrollArea->setWidgetResizable(true);

    childrenOverview = new QGroupBox;
    QVBoxLayout * childrenOverviewLayout = new QVBoxLayout;
    childrenOverview->setLayout(childrenOverviewLayout);

    childrenOverviewScrollArea->setWidget(childrenOverview);

    childrenOverviewDock = new QDockWidget(tr("Children overview"), this);
    childrenOverviewDock->setObjectName(tr("Children overview"));
    childrenOverviewDock->setWidget(childrenOverviewScrollArea);
    addDockWidget(Qt::RightDockWidgetArea, childrenOverviewDock);
    ui->menuView->addAction(childrenOverviewDock->toggleViewAction());
}

void MainWindow::setupToolBar()
{
    // add child/attribute button
    {
        QAction * action = new QAction(ui->mainToolBar);
        action->setIcon(QIcon(":/icons/icons/resources/16x16/add.png"));
        action->setToolTip(tr("Add a child node or an attribute to the selected node"));

        connect(action, &QAction::triggered, this, [this](){
            if(attributeList->selectionModel()->hasSelection())
            {
                nodesView->addAttribute(attributeList->currentItem()->data(0, Qt::UserRole).toString());
            }
            else if(childrenList->selectionModel()->hasSelection())
            {
                nodesView->addChild(childrenList->currentItem()->data(0, Qt::UserRole).toString());
            }
        });
        ui->mainToolBar->addAction(action);
    }
    // remove node button
    {
        QAction * action = new QAction(ui->mainToolBar);
        action->setIcon(QIcon(":/icons/icons/resources/16x16/delete.png"));
        action->setToolTip(tr("Remove selected node"));
        connect(action, &QAction::triggered, nodesView, &CustomNodesView::removeNodes);
        ui->mainToolBar->addAction(action);
    }
    // remove attribute button
    {
        QAction * action = new QAction(ui->mainToolBar);
        action->setIcon(QIcon(":/icons/icons/resources/16x16/nodes_delete_selected.png"));
        action->setToolTip(tr("Remove selected attribute"));
        connect(action, &QAction::triggered, attributesView, &CustomAttributesView::removeAttribute);
        ui->mainToolBar->addAction(action);
    }
    // duplicate button
    {
        QAction * action = new QAction(ui->mainToolBar);
        action->setIcon(QIcon(":/icons/icons/resources/16x16/node_duplicate.png"));
        action->setToolTip(tr("Duplicate selected node"));
        connect(action, &QAction::triggered, nodesView, &CustomNodesView::duplicateNodes);
        ui->mainToolBar->addAction(action);
    }
    // move up button
    {
        QAction * action = new QAction(ui->mainToolBar);
        action->setIcon(QIcon(":/icons/icons/resources/16x16/move_child_up.png"));
        action->setToolTip(tr("Move up selected node"));
        connect(action, &QAction::triggered, nodesView, &CustomNodesView::moveUpNode);
        ui->mainToolBar->addAction(action);
    }
    // move down button
    {
        QAction * action = new QAction(ui->mainToolBar);
        action->setIcon(QIcon(":/icons/icons/resources/16x16/move_child_down.png"));
        action->setToolTip(tr("Move down selected node"));
        connect(action, &QAction::triggered, nodesView, &CustomNodesView::moveDownNode);
        ui->mainToolBar->addAction(action);
    }
    // validate
    {
        QAction * action = new QAction(ui->mainToolBar);
        action->setIcon(QIcon(":/icons/icons/resources/16x16/validate_memmap.png"));
        action->setToolTip(tr("Validate memory map"));
        connect(action, &QAction::triggered, this, &MainWindow::validate);
        ui->mainToolBar->addAction(action);
    }
    ui->mainToolBar->addSeparator();

    ui->mainToolBar->addActions(python_buttons);
    ui->menuView->addAction(ui->mainToolBar->toggleViewAction());
}

bool MainWindow::getPromptSubmap()
{
    return prompt_submap;
}

bool MainWindow::getPromptMainmap()
{
    return prompt_mainmap;
}

void MainWindow::setPromptMainmap(bool new_val)
{
    prompt_mainmap = new_val;
}

void MainWindow::setPromptSubmap(bool new_val)
{
    prompt_submap = new_val;
}


void MainWindow::setDocksAndActionsEnabled(bool enabled)
{
    // docks
    nodesViewDock->setEnabled(enabled);
    attributesViewDock->setEnabled(enabled);
    childrenOverviewDock->setEnabled(enabled);
    childrenListDock->setEnabled(enabled);
    attributeListDock->setEnabled(enabled);
    ui->mainToolBar->setEnabled(enabled);
    // actions
    ui->actionValidate->setEnabled(enabled);
    ui->actionFind->setEnabled(enabled);
    ui->actionSaveAs->setEnabled(enabled);
    ui->actionSaveFile->setEnabled(enabled);
    ui->actionClose_file->setEnabled(enabled);
    ui->menuActions->setEnabled(enabled);
}

void MainWindow::verticalResizeTableViewToContents(QTableView *tableView)
{
    int rowTotalHeight=0;

    // Rows height
    int count=tableView->verticalHeader()->count();
    for (int i = 0; i < count; ++i) {
        // 2018-03 edit: only account for row if it is visible
        if (!tableView->verticalHeader()->isSectionHidden(i)) {
            rowTotalHeight+=tableView->verticalHeader()->sectionSize(i);
        }
    }

    // Check for scrollbar visibility
    if (!tableView->horizontalScrollBar()->isHidden())
    {
         rowTotalHeight+=tableView->horizontalScrollBar()->height();
    }

    // Check for header visibility
    if (!tableView->horizontalHeader()->isHidden())
    {
         rowTotalHeight+=tableView->horizontalHeader()->height();
    }
    tableView->setMinimumHeight(rowTotalHeight);
}

void MainWindow::on_actionAbout_triggered()
{
	QString msg = QCoreApplication::applicationName() + " " +
		QCoreApplication::applicationVersion() + "<br>"
		"Build " + QString::fromLocal8Bit(TODAY) + "<br>"
		"SY-RF-CS<br>" +
		QCoreApplication::organizationName() + "<br>"
		"reksio-support@cern.ch<br><br>"
		"Python dependiencies (based on git tag):"
		"<ul>"
		"<li> PyCheby " + QString(PYCHEBY_VERSION) + "</li>"
		"<li> cheby<sup>*</sup> " + QString(CHEBY_VERSION) + "</li>"
		"<li> cheburashka<sup>**,***</sup> " + QString(CODE_GEN_VERSION) + "</li>"
		"</ul>"
		"Icon made by Freepik from www.flaticon.com<br><br>"
		"<small>* Tag not always correspond to the package version </small><br>"
		"<small>** Available only for CERN builds</small><br>"
		"<small>***Called also as 'Code-Generators'</small><br>";

	QMessageBox::about(this, "About", msg);
}

void MainWindow::on_actionSettings_triggered()
{
    settings_dialog->show();
    settings_dialog->activateWindow();
    settings_dialog->raise();
    settings_dialog->setWindowState(Qt::WindowActive);
}

void MainWindow::on_actionOpen_autosave_location_triggered()
{
    QDesktopServices::openUrl(QUrl::fromLocalFile(autosave_path));
}

void MainWindow::on_actionValidate_triggered()
{
    validate();
}

void MainWindow::on_actionRestore_layout_triggered()
{
    QSettings settings;
    settings.setValue("restore", true);
}

void MainWindow::on_actionOpen_schema_file_location_triggered()
{
    QDesktopServices::openUrl(QUrl::fromLocalFile(qApp->applicationDirPath() + "/schema/"));
}

void MainWindow::on_actionOpen_configuration_file_location_triggered()
{
    QDesktopServices::openUrl(QUrl::fromLocalFile(qApp->applicationDirPath() + "/settings/"));
}

void MainWindow::on_actionOpen_Python_scripts_location_triggered()
{
    QDesktopServices::openUrl(QUrl::fromLocalFile(qApp->applicationDirPath() + "/python_scripts/"));
}

void MainWindow::on_actionOpen_user_settings_location_triggered()
{
    const QSettings settings;
    QDesktopServices::openUrl(QUrl::fromLocalFile(QFileInfo(settings.fileName()).path()));
}

void MainWindow::on_actionReload_schema_triggered()
{
    QMessageBox::StandardButton decision;
    decision = QMessageBox::question(this, tr("Are you sure?"), tr("Do you want to reload a schema? This will close any opened files"));
    if(decision == QMessageBox::Yes)
    {
        if(isModified())
        {
            QMessageBox::StandardButton decision = saveDiscardCancel();
            if(decision == QMessageBox::Cancel)
                return;
        }
        clearState();
        loadSchema();
    }
}

void MainWindow::on_actionClose_file_triggered()
{
    if(isModified())
    {
        QMessageBox::StandardButton decision = saveDiscardCancel();
        if(decision == QMessageBox::Cancel)
        {
            return;
        }
    }
    clearState();
}
